﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Numerics;
using System.Diagnostics;
using System.IO;
using System.Security.Cryptography;

namespace BitcoinLibrary
{
    /**
    * A collection of various utility methods that are helpful for working with the BitCoin protocol.
    * To enable debug logging from the library, run with -Dbitcoinj.logging=true on your command line.
    */
    public class Utils
    {
        // TODO: Replace this nanocoins business with something better.

        /**
        * How many "nanocoins" there are in a BitCoin.
        *
        * A nanocoin is the smallest unit that can be transferred using BitCoin.
        * The term nanocoin is very misleading, though, because there are only 100 million
        * of them in a coin (whereas one would expect 1 billion.
        */
        public static readonly BigInteger COIN = new BigInteger(100000000);

        /**
        * How many "nanocoins" there are in 0.01 BitCoins.
        *
        * A nanocoin is the smallest unit that can be transferred using BitCoin.
        * The term nanocoin is very misleading, though, because there are only 100 million
        * of them in a coin (whereas one would expect 1 billion).
        */
        public static readonly BigInteger CENT = new BigInteger(1000000);

        /** Convert an amount expressed in the way humans are used to into nanocoins. */
        public static BigInteger toNanoCoins(int coins, int cents)
        {
            Debug.Assert(cents < 100);
            BigInteger bi = BigInteger.Multiply(new BigInteger(coins), COIN);
            bi = BigInteger.Add(bi, BigInteger.Multiply(new BigInteger(cents), CENT));
            return bi;
        }

        /**
        * Convert an amount expressed in the way humans are used to into nanocoins.<p>
        *
        * This takes string in a format understood by {@link BigDecimal#BigDecimal(String)},
        * for example "0", "1", "0.10", "1.23E3", "1234.5E-5".
        * 
        * @throws ArithmeticException if you try to specify fractional nanocoins
        **/
        public static BigInteger toNanoCoins(String coins)
        {
            // TODO: return new BigDecimal(coins).movePointRight(8).toBigIntegerExact();
            throw new Exception("NYI");
        }

        public static void uint32ToByteArrayBE(long val, byte[] outr, int offset)
        {
            outr[offset + 0] = (byte)(0xFF & (val >> 24));
            outr[offset + 1] = (byte)(0xFF & (val >> 16));
            outr[offset + 2] = (byte)(0xFF & (val >> 8));
            outr[offset + 3] = (byte)(0xFF & (val >> 0));
        }

        public static void uint32ToByteArrayLE(long val, byte[] outr, int offset)
        {
            outr[offset + 0] = (byte)(0xFF & (val >> 0));
            outr[offset + 1] = (byte)(0xFF & (val >> 8));
            outr[offset + 2] = (byte)(0xFF & (val >> 16));
            outr[offset + 3] = (byte)(0xFF & (val >> 24));
        }

        public static void uint32ToByteStreamLE(long val, Stream stream)
        {
            stream.Write(new byte[] { (byte)(0xFF & (val >> 0)), (byte)(0xFF & (val >> 8)), (byte)(0xFF & (val >> 16)), (byte)(0xFF & (val >> 24)) }, 0, 4);
        }

        public static void uint64ToByteStreamLE(BigInteger val, Stream stream)
        {
            byte[] bytes = val.ToByteArray().Reverse().ToArray();
            if (bytes.Length > 8)
            {
                throw new Exception("Input too large to encode into a uint64");
            }
            bytes = bytes.Reverse().ToArray();
            stream.Write(bytes, 0, bytes.Length);
            if (bytes.Length < 8)
            {
                for (int i = 0; i < 8 - bytes.Length; i++)
                    stream.Write(new byte[] { (byte)0 }, 0, 1);
            }
        }

        /**
        * See {@link Utils#doubleDigest(byte[],int,int)}.
        */
        public static byte[] doubleDigest(byte[] input)
        {
            return doubleDigest(input, 0, input.Length);
        }

        /**
        * Calculates the SHA-256 hash of the given byte range, and then hashes the resulting hash again. This is
        * standard procedure in BitCoin. The resulting hash is in big endian form.
        */
        public static byte[] doubleDigest(byte[] input, int offset, int length)
        {
            var sha256 = new SHA256Managed();
            var hash = sha256.ComputeHash(sha256.ComputeHash(input, offset, length));
            return hash;
            //try
            //{
            //    MessageDigest digest = MessageDigest.getInstance("SHA-256");
            //    digest.update(input, offset, length);
            //    byte[] first = digest.digest();
            //    return digest.digest(first);
            //}
            //catch (NoSuchAlgorithmException e)
            //{
            //    throw new RuntimeException(e); // Cannot happen.
            //}
        }

        /**
        * Calculates SHA256(SHA256(byte range 1 + byte range 2)).
        */
        public static byte[] doubleDigestTwoBuffers(byte[] input1, int offset1, int length1,
        byte[] input2, int offset2, int length2)
        {
            throw new Exception("NYI");
            //try
            //{
            //    MessageDigest digest = MessageDigest.getInstance("SHA-256");
            //    digest.update(input1, offset1, length1);
            //    digest.update(input2, offset2, length2);
            //    byte[] first = digest.digest();
            //    return digest.digest(first);
            //}
            //catch (Exception e)
            //{
            //    throw new Exception(e); // Cannot happen.
            //}
        }

        /** Work around lack of unsigned types in Java. */
        public static bool isLessThanUnsigned(long n1, long n2)
        {
            return (n1 < n2) ^ ((n1 < 0) != (n2 < 0));
        }

        /** Returns the given byte array hex encoded. */
        public static String bytesToHexString(byte[] bytes)
        {
            StringBuilder buf = new StringBuilder(bytes.Length * 2);
            foreach (byte b in bytes)
            {
                String s = ((int)0xFF & b).ToString("x");//  Integer.toString(0xFF & b, 16);    // TODO: correct?
                if (s.Length < 2)
                {
                    buf.Append('0');
                }
                buf.Append(s);
            }
            return buf.ToString();
        }


        /** Returns a copy of the given byte array in reverse order. */
        public static byte[] reverseBytes(byte[] bytes)
        {
            return bytes.Reverse().ToArray();
        }

        public static long readUint32(byte[] bytes, int offset)
        {
            return ((bytes[offset++] & 0xFFL) << 0) |
            ((bytes[offset++] & 0xFFL) << 8) |
            ((bytes[offset++] & 0xFFL) << 16) |
            ((bytes[offset] & 0xFFL) << 24);
        }

        public static long readUint32BE(byte[] bytes, int offset)
        {
            return ((bytes[offset + 0] & 0xFFL) << 24) |
            ((bytes[offset + 1] & 0xFFL) << 16) |
            ((bytes[offset + 2] & 0xFFL) << 8) |
            ((bytes[offset + 3] & 0xFFL) << 0);
        }

        public static int readUint16BE(byte[] bytes, int offset)
        {
            return ((bytes[offset] & 0xff) << 8) | bytes[offset + 1] & 0xff;
        }

        /**
        * Calculates RIPEMD160(SHA256(input)). This is used in Address calculations.
        */
        public static byte[] sha256hash160(byte[] input)
        {

            var sha256 = new SHA256Managed();
            var hash = sha256.ComputeHash(input, 0, input.Length);
            var ripemd160 = RIPEMD160Managed.Create();
            hash = ripemd160.ComputeHash(hash, 0, hash.Length);
            return hash;
            //try {
            //byte[] sha256 = MessageDigest.getInstance("SHA-256").digest(input);
            //RIPEMD160Digest digest = new RIPEMD160Digest();
            //digest.update(sha256, 0, sha256.length);
            //byte[] out = new byte[20];
            //digest.doFinal(out, 0);
            //return out;
            //} catch (NoSuchAlgorithmException e) {
            //throw new RuntimeException(e); // Cannot happen.
            //}
        }

        /** Returns the given value in nanocoins as a 0.12 type string. */
        public static String bitcoinValueToFriendlyString(BigInteger value)
        {
            bool negative = value < 0;// value.compareTo(BigInteger.ZERO) < 0;
            if (negative)
            {
                value = -value;
            }
            BigInteger coins = value / COIN;
            BigInteger cents = value % COIN;
            //return String.format("%s%d.%02d", negative ? "-" : "", coins.intValue(), cents.intValue() / 1000000);
            return String.Format("{0}{1}.{2}", negative ? "-" : "", (int)coins, (int)cents / 1000000);  // TODO: fix format of {2}
        }

        /**
        * MPI encoded numbers are produced by the OpenSSL BN_bn2mpi function. They consist of
        * a 4 byte big endian length field, followed by the stated number of bytes representing
        * the number in big endian format.
        */
        static BigInteger decodeMPI(byte[] mpi)
        {
            int length = (int)readUint32BE(mpi, 0);
            byte[] buf = new byte[length];
            Array.Copy(mpi, 4, buf, 0, length);
            return new BigInteger(buf);
        }

        // The representation of nBits uses another home-brew encoding, as a way to represent a large
        // hash value in only 32 bits.
        public static BigInteger decodeCompactBits(long compact)
        {
            int size = ((int)(compact >> 24)) & 0xFF;
            byte[] bytes = new byte[4 + size];
            bytes[3] = (byte)size;
            if (size >= 1) bytes[4] = (byte)((compact >> 16) & 0xFF);
            if (size >= 2) bytes[5] = (byte)((compact >> 8) & 0xFF);
            if (size >= 3) bytes[6] = (byte)((compact >> 0) & 0xFF);
            return decodeMPI(bytes);
        }

        /// <summary>
        /// Method for converting a System.DateTime value to a UNIX Timestamp
        /// </summary>
        /// <param name="value">date to convert</param>
        /// <returns></returns>
        public static UInt32 ConvertToTimestamp(DateTime value)
        {
            // Create Timespan by subtracting the value provided from the Unix Epoch
            TimeSpan span = (value - new DateTime(1970, 1, 1, 0, 0, 0, 0).ToLocalTime());

            // return the total seconds (which is a UNIX timestamp)
            return (UInt32)span.TotalSeconds;
        }

        public static string GetEclipseArray(byte[] bits)
        {
            StringBuilder sb = new StringBuilder();
            foreach (var b in bits) sb.Append((b > 128 ? -(256 - b) : b) + ", ");
            string res = sb.ToString();
            return res;
        }
    }
}
